Skip to content

KubeJS 兼容

「顿悟 Epiphany」提供了完整的 KubeJS 兼容,允许整合包作者通过 JS 脚本监听事件、调用 API 和查询数据。所有功能通过标准 KubeJS 插件机制注册。

TIP

所有脚本均位于 server_scripts/ 目录,仅在服务端运行。

INFO

本章节介绍的所有概念与 自定义事件Manager API 一一对应。如未特别说明,行为与 Java 端完全一致。

总览

Epiphany 通过 KubeJS 插件提供两种入口:

命名空间用途来源
EpiphanyEvents.*监听事件(Module/Insight/Epiphany/Aptitude 相关的 15 个事件)KubeJS EventGroup
Epiphany.*调用 API(Manager 方法 + 数据查询)KubeJS Binding

命名分离

事件监听用 EpiphanyEvents.*,API 调用用 Epiphany.*。两者刻意分到不同命名空间,避免重名冲突。

插件入口:ink.myumoon.epiphany.event.kubejs.EpiphanyKubeJSPlugin,在 kubejs.plugins.txt 中被发现。

事件(EpiphanyEvents.*)

所有事件均携带 player 字段(ServerPlayer)。每个事件在发生时携带相关的 id 字段(如 moduleId / insightId / epiphanyId),供脚本读取。

命名约定

后缀含义可取消
现在时(如 moduleUnlock / moduleSelect / moduleComplete)Pre 事件,通过 event.cancel() 取消
过去时(如 moduleUnlocked / moduleSelected / moduleCompleted)Post 事件,仅通知

取消机制

Pre 事件取消使用 KubeJS 标准的 event.cancel(),不是 Java 端的 setCanceled(true)。插件内部会自动桥接到原生 NeoForge 事件。

Module 事件

事件名类型触发时机
moduleUnlockPre(可取消)模块即将解锁时
moduleUnlockedPost模块已解锁时
moduleSelectPre(可取消)模块即将被选择时
moduleSelectedPost模块已被选择时
moduleCompletePre(可取消)模块即将完成时
moduleCompletedPost模块已完成时

Insight 事件

级联

insightSelected 触发后,若所属模块的所有心得因此全部点亮,会自动连锁触发 Module 的 moduleComplete / moduleCompleted。一次 insightSelect 调用可能引发完整的 4 事件链。

事件名类型描述
insightSelectPre(可取消)心得即将被点亮时
insightSelectedPost心得已被点亮时

Epiphany 事件

事件名类型描述
epiphanyUnlockPre(可取消)顿悟即将解锁时
epiphanyUnlockedPost顿悟已解锁时
epiphanySelectPre(可取消)顿悟即将被激活时
epiphanySelectedPost顿悟已被激活时

Aptitude 事件(全部 Post)

触发频率差异

aptitudeLevelUp 每次 +1 都会单独触发(一次 addAptitude 可能触发多次)。而 insightPointsChanged结算级事件,无论一次调用内部升级多少次,只触发一次。通知类用后者,逐次记录用前者。

事件名描述
aptitudeChanged阅历值变动时
aptitudeLevelUp阅历条充满、获得心得点时(每 +1 一次)
insightPointsChanged心得点余额变动时(结算级一次)

绑定(Epiphany.*)

所有方法均在服务端同步执行。player 参数必须是 ServerPlayer 类型。

工具

js
Epiphany.id(namespace, path)   // 构造 ResourceLocation
Epiphany.id('mymod', 'foo')    // → 'mymod:foo'

也可以直接用 KubeJS 字面量字符串 'mymod:foo',两者等价。

Module

方法描述
isModuleUnlocked(player, id)查询:模块是否已解锁
isModuleSelected(player, id)查询:模块是否已选择
isModuleCompleted(player, id)查询:模块是否已完成
moduleUnlock(player, id)解锁模块
moduleLock(player, id)锁定模块
moduleSelect(player, id)选择模块(消耗 心得点 )
moduleForceSelect(player, id)强制选择(忽略消耗/条件/上限)
moduleComplete(player, id)完成模块(需所有心得已点亮)
moduleForceComplete(player, id)强制完成
moduleReset(player, id)重置模块(退还 心得点 ,移除奖励)

Insight

方法描述
isInsightSelected(player, id)查询:心得是否已点亮
isInsightModuleSelected(player, id)查询:心得所属模块是否已选
insightSelect(player, insightId, moduleId)点亮心得(消耗心得点)
insightForceSelect(player, insightId, moduleId)强制点亮心得(忽略消耗/前置)
insightReset(player, id)重置心得

Epiphany

方法描述
isEpiphanyUnlocked(player, id)查询:顿悟是否已解锁
isEpiphanySelected(player, id)查询:顿悟是否已激活
epiphanyUnlock(player, id)解锁顿悟
epiphanyLock(player, id)锁定顿悟
epiphanySelect(player, id)激活顿悟(消耗 1 槽位)
epiphanyForceSelect(player, id)强制激活(忽略槽位限制)
epiphanyReset(player, id)重置顿悟

辅助功能

方法返回描述
getEpiphanySlots(player)int玩家已解锁的顿悟槽数量
getUsedEpiphanySlots(player)int当前已占用的顿悟槽数量
getMaxEpiphanySlots()int配置中允许选择的顿悟数量上限
getSelectedModuleCount(player)int玩家当前已选择的模块数量
getMaxSelectedModules()int配置中允许同时选择的模块数量上限

重置

方法描述
resetAll(player)移除全部奖励和顿悟数据,再恢复初始状态为 SELECTABLE 的条目
resetSelections(player)清除选择和奖励,保留阅历并退还选择消耗,再恢复初始状态为 SELECTABLE 的条目

事件行为

这两个方法分别等价于 /epiphany reset all/epiphany reset select,且不会触发阅历、心得点或选择事件。

Aptitude / Points

方法返回描述
getAptitude(player)long当前阅历
getInsightPoints(player)int可用心得点
getTotalInsightPointsSpent(player)int累计已花费心得点
setAptitude(player, value)设置阅历
addAptitude(player, amount)精确增加阅历,不应用获取倍率(可能触发升级)
addAptitudeWithMultiplier(player, amount)按玩家的 aptitude_gain_multiplier 增加阅历,缩放结果向下取整
setInsightPoints(player, value)设置心得点
addInsightPoints(player, amount)增加或扣除心得点(结果截断到 >= 0
fillAptitude(player)long将阅历补充到下一心得点阈值,并返回实际增加量
getRequiredForNextPoint(player)long获取玩家下一个心得点的阅历阈值
calcRequiredAptitude(totalSpent, points)long计算下一心得点所需阅历

Aptitude Source(自定义阅历来源)

方法返回描述
grantAptitude(player, behaviorId, targetId, registry)boolean按数据包规则发放阅历
resolveAptitudeSource(player, behaviorId, targetId, registry)Resolution纯查询

registry 参数传 null 即可(用于解析 tag 引用)。

数据查询(Datapack Registry)

按 ID 查询单个数据包定义,查不到时返回 null

方法返回类型
getModule(id)ModuleData
getInsight(id)InsightData
getEpiphany(id)EpiphanyData
getPath(id)PathData

也可以列出所有已注册的模块、顿悟或心得:

方法返回类型描述
allModuleIds()ResourceLocation[]所有模块 ID,已排序
allModules()ModuleData[]所有模块定义
allEpiphanyIds()ResourceLocation[]所有顿悟 ID,已排序
allEpiphanies()EpiphanyData[]所有顿悟定义
allInsightIds()ResourceLocation[]所有心得 ID,已排序
allInsights()InsightData[]所有心得定义

按数组遍历

批量查询返回 Java 数组而不是 List。请在 KubeJS 中使用数组下标;Rhino 的 List.get(i) 可能返回错误的元素。

js
let ids = Epiphany.allModuleIds();
for (let i = 0; i < ids.length; i++) {
    console.info(ids[i]);
}

let insights = Epiphany.allInsights();
for (let i = 0; i < insights.length; i++) {
    console.info(insights[i].cost());
}

返回的 record 可以直接访问字段,例如:

js
let module = Epiphany.getModule('mymod:combat');
if (module) {
    console.info(module.name);          // Optional<Component>
    console.info(module.description);   // Optional<Component>
    console.info(module.initialState);  // 'locked' 或 'selectable'
    console.info(module.weight);        // number
    module.insights.forEach(entry => {
        console.info(entry.id, entry.depth);  // 心得 ID + 层级
    });
}

注意 Optional 与 lang 回退

record 暴露的 name / description / *_description 字段都是原始 Optional,即数据包 JSON 里填了什么就是什么,没有应用缺省 lang 回退逻辑。例:

  • module.nameOptional<Component>,可能为空(若 JSON 未写 name)
  • module.name.get().getString() → 取出 JSON 字面值(或翻译键字符串),不会走 lang 翻译回退

如果你通过资源包提供多语言支持,请用各数据类型提供的 effectiveXxx(id) 方法:

js
let id = 'mymod:combat';
let module = Epiphany.getModule(id);
if (module) {
    // 为空
    let rawName = module.name;  // Optional<Component>

    // 返回 lang
    let displayName = module.effectiveName(id).getString();
    let displayDesc = module.effectiveDescription(id).getString();
}

每个类型提供的 effectiveXxx 方法对应数据包的 Component 字段(name / description / condition_description / reward_description 等),缺省时按约定翻译键查询 assets/<ns>/lang/*.json。命名规则见 Module · 文本组件与 i18n

KubeJS Stage 条件

Epiphany 提供了基于 KubeJS Stage 的 condition 和 Reward 类型,可在数据包 JSON 中使用:

json
{ "type": "epiphany:kubejs_stage", "stage": "nether_access" }

当玩家拥有指定的 KubeJS Stage 时,该条件成立。未安装 KubeJS 时,此条件永远返回 false,不会崩溃。

示例脚本

监听模块选择并发放欢迎奖励

js
// server_scripts/welcome.js

EpiphanyEvents.moduleSelected(event => {
    let player = event.player;
    let moduleId = event.moduleId;

    // 给玩家发送提示
    player.tell('你已选择模块: ' + moduleId);

    // 示例:首次选择战斗模块时给一把铁剑
    if (moduleId === 'mymod:combat') {
        let hasClaimed = player.stages.has('combat_welcome');
        if (!hasClaimed) {
            player.give('minecraft:iron_sword');
            player.stages.add('combat_welcome');
            player.tell('欢迎加入战士之路!获得:铁剑 x1');
        }
    }
});

Pre 事件:阻止某些模块被选

js
// server_scripts/restrict.js

EpiphanyEvents.moduleSelect(event => {
    let player = event.player;

    // 示例:在 Boss 战期间禁止选择任何模块
    if (player.stages.has('boss_fight_active')) {
        event.cancel();
        player.tell('Boss 战进行中,无法选择新模块。');
    }
});

// 顿悟同样规则
EpiphanyEvents.epiphanySelect(event => {
    if (event.player.stages.has('boss_fight_active')) {
        event.cancel();
    }
});

阅历监听 + 通知

js
// server_scripts/aptitude_notify.js

// 玩家获得心得点时提示
EpiphanyEvents.insightPointsChanged(event => {
    if (event.isGain) {
        event.player.tell(
            `获得心得点!当前: ${event.newValue} (+'${event.delta})`
        );
    }
});

// 每次阅历升级单独记录
let levelUpCounter = {};
EpiphanyEvents.aptitudeLevelUp(event => {
    let uuid = event.player.uuid;
    levelUpCounter[uuid] = (levelUpCounter[uuid] || 0) + 1;
    // 每 10 次升级给奖励
    if (levelUpCounter[uuid] % 10 === 0) {
        event.player.give('minecraft:diamond');
    }
});

通过 API 操作玩家数据

js
// server_scripts/admin_helper.js

PlayerEvents.loggedIn(event => {
    let player = event.player;

    // 查询玩家进度
    let pts = Epiphany.getInsightPoints(player);
    let apt = Epiphany.getAptitude(player);
    console.info(`[Epiphany] ${player.name} 心得点=${pts}, 阅历=${apt}`);

    // 首次登录赠送 100 阅历
    if (!player.stages.has('epiphany_gift')) {
        Epiphany.addAptitude(player, 100);
        player.stages.add('epiphany_gift');
        player.tell('欢迎礼物:阅历 +100');
    }
});

查询数据包定义

js
// server_scripts/inspect.js

// 列出某模块的所有心得及其层级
let moduleId = 'mymod:combat';
let module = Epiphany.getModule(moduleId);
if (!module) {
    console.warn('Module not found: ' + moduleId);
} else {
    console.info(`Module: ${moduleId}, weight=${module.weight}`);
    module.insights.forEach(entry => {
        let insight = Epiphany.getInsight(entry.id);
        if (!insight) {
            console.info(`  - depth=${entry.depth}: ${entry.id} → (缺失)`);
        } else {
            let desc = insight.effectiveDescription(entry.id.toString()).getString();
            console.info(`  - depth=${entry.depth}: ${entry.id} → ${desc}`);
        }
    });
}

自定义阅历来源

js
// server_scripts/custom_aptitude.js

// 假设你的数据包中定义了:
// data/mymod/epiphany/aptitude/fishing.json
// { "default": 2, "specials": [{ "target": "minecraft:cod", "reward": 5 }] }

// 监听原版钓鱼事件,发放 Epiphany 阅历
FishingHookEvent(event => {
    let player = event.player;
    let caught = event.stack;  // 钓到的物品
    let targetId = caught.getItem().getRegistryName(); // ResourceLocation
    Epiphany.grantAptitude(
        player,
        Epiphany.id('mymod', 'fishing'),
        targetId,
        BuiltInRegistries.ITEM
    );
});

WARNING

这是一个假想事件,实际 KubeJS 中没有 FishingHookEvent,也未经过实际测试。请根据实际情况使用正确的事件类型。